package mn.more.wits.server;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;
import java.util.Date;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import mn.more.wits.client.AuthProvider;
import mn.more.wits.client.dto.AuthDTO;
import mn.more.wits.client.dto.ChangePasswordDTO;
import mn.more.wits.client.dto.Payload;
import mn.more.wits.client.exception.AccountDisabledException;
import mn.more.wits.client.exception.AlreadyInSessionException;
import mn.more.wits.client.exception.AlreadyLoggedInException;
import mn.more.wits.client.exception.LoginRequiredException;
import mn.more.wits.client.exception.WitsSecurityException;
import mn.more.wits.server.dao.AuthProviderDAO;
import mn.more.wits.server.model.AccountHistory;
import mn.more.wits.server.model.AccountStatus;
import mn.more.wits.server.model.WitsUser;
import mn.more.wits.server.util.ClientModelEncryptor;
import org.apache.log4j.Logger;
import org.gwtwidgets.server.spring.ServletUtils;
import org.springframework.context.MessageSource;

/**
 * @author $Author: mikeliucc $
 * @version $Id: AuthProviderImpl.java 5 2008-09-01 12:08:42Z mikeliucc $
 */
public class AuthProviderImpl extends RemoteServiceServlet implements AuthProvider {

	private static final String DELIM = "|";

	private transient Logger logger = Logger.getLogger(AuthProviderImpl.class);
	private transient Logger auditLogger;
	private transient MessageSource messages;
	private AuthProviderDAO dao;
	private ClientModelEncryptor clientModelEncryptor;

	public void setAuditLogger(Logger auditLogger) { this.auditLogger = auditLogger; }

	public void setMessages(MessageSource messages) { this.messages = messages; }

	public void setClientModelEncryptor(ClientModelEncryptor clientModelEncryptor) {
		this.clientModelEncryptor = clientModelEncryptor;
	}

	public void setDao(AuthProviderDAO dao) { this.dao = dao; }

	public Payload authenticate(Payload payload) throws WitsSecurityException {
		if (logger.isDebugEnabled()) { logger.debug("AuthProviderImpl.authenticate"); }

		AuthDTO auth = new AuthDTO();
		clientModelEncryptor.toModel(payload, auth);
		HttpServletRequest request = ServletUtils.getRequest();
		audit("auth.attempt", request, auth);

		assertNewLogin(request, auth);

		// not logged in yet, let's see what (s)he is trying to do...

		// we should NOT report this as an error because user (hacker) might use this information to
		// exploit the system.  Instead, let's just return null so it looks like a wrong username/password
		// combination.
		if (!isAccountEnabled(request, auth)) { return null; }
		assertActiveAccount(request, auth);
		assertNoConcurrentLogin(request, auth);

		// ok, now we'll REALLY check for the username/password combination
		WitsUser user = dao.authenticated(auth.getUsername(), auth.getPassword());
		if (user == null) {
			incrementFailCount(request);
			// todo: need to track failed attempts that are between 5 minutes apart
			// todo: needs to handle failed attempts more than 3
			auditFailCount(request, auth);
			return null;
		}

		// at this point, the account is NOT disabled/locked, there's no in-session use, and the username/password
		// combination is correct.
		audit("authSuccess", user, user.getDisplayName(), new Date().toString());
		return clientModelEncryptor.toPayload(user.toUserModel());
	}

	public Payload getAuth() {
		WitsUser acct = SecurityUtil.getCurrentUser();
		if (acct == null) { return null; }
		return clientModelEncryptor.toPayload(acct.toUserModel());
	}

	public void invalidate() throws WitsSecurityException {
		if (logger.isDebugEnabled()) { logger.debug("AuthProviderImpl.invalidate"); }

		HttpSession session = ServletUtils.getRequest().getSession(true);
		if (session != null) {
			WitsUser user = getCurrentAccount(ServletUtils.getRequest());
			if (user != null) {
				audit("logout.inprogress", user);
				// todo: add message as constant
				dao.invalidate(user, "Logout/Session timed out");
				audit("logout.success", user);
			} else {
				if (logger.isInfoEnabled()) { logger.info("user requested logout, but currently not logged in."); }
			}
		}
	}

	public void changePassword(Payload payload) throws WitsSecurityException {
		HttpServletRequest request = ServletUtils.getRequest();
		WitsUser user = getCurrentAccount(request);
		if (user == null) { throw new LoginRequiredException(); }

		ChangePasswordDTO changePassword = new ChangePasswordDTO();
		clientModelEncryptor.toModel(payload, changePassword);
		audit("auth.changePassword", user);

		assertActiveAccount(user);

		if (dao.authenticated(user.getLoginId(), changePassword.getCurrentPassword()) == null) {
			// authentication with current password failed!
			// we shouldn't give out too much information because it could be used as a form of securty leak
			throw new LoginRequiredException();
		} else {
			// authenticated, now change password
			dao.changePassword(user.getLoginId(), changePassword.getNewPassword());
		}
	}

	/** check for locked or disabled account */
	private boolean isAccountEnabled(HttpServletRequest request, AuthDTO auth) {
		AccountStatus authStatus = dao.getAccountStatus(auth.getUsername());
		if (authStatus != null) {
			return AccountStatus.ACTIVE == authStatus;
		} else {
			// such account doesn't exist... no point to continue
			audit("auth.noSuchAccount", request, auth);
			return false;
		}
	}

	/** check for any in-session use of the same account */
	private void assertNoConcurrentLogin(HttpServletRequest request, AuthDTO auth) throws AlreadyInSessionException {
		String username = auth.getUsername();
		AccountHistory inSession = dao.getInSessionDetail(username);
		if (inSession != null) {
			// account is currently in use
			audit("auth.alreadyInSession", request, auth, inSession.getSessionId());
			// user will be notified that account is in use, but not if the provided username/password combination
			// is correct or not.  This should be more secured than to actually check the username/password first.
			AlreadyInSessionException e = new AlreadyInSessionException();
			e.setInSessionId(inSession.getSessionId());
			throw e;
		}
	}

	/** check for currently logged in user */
	private void assertNewLogin(HttpServletRequest request, AuthDTO auth) throws AlreadyLoggedInException {
		WitsUser user = getCurrentAccount(request);
		if (user != null) {
			// user already in... so he/she cannot login again until (s)he logs out first
			audit("auth.alreadyLoggedIn", request, auth);
			throw new AlreadyLoggedInException();
		}
	}

	/** check if the targeted account is active or not */
	private void assertActiveAccount(WitsUser user) throws AccountDisabledException {
		AccountStatus authStatus = dao.getAccountStatus(user.getLoginId());
		if (AccountStatus.ACTIVE != authStatus) {
			// account is not active
			audit("auth.accountInactive", user);
			// user will be notified that account is diabled, but not if provided username/password combination is
			// correct or not.
			throw new AccountDisabledException();
		}
	}

	/** check if the targeted account is active or not */
	private void assertActiveAccount(HttpServletRequest request, AuthDTO auth) throws AccountDisabledException {
		String username = auth.getUsername();
		AccountStatus authStatus = dao.getAccountStatus(username);
		if (AccountStatus.ACTIVE != authStatus) {
			// account is not active
			audit("auth.accountInactive", request, auth);
			// user will be notified that account is diabled, but not if provided username/password combination is
			// correct or not.
			throw new AccountDisabledException();
		}
	}

	private static WitsUser getCurrentAccount(HttpServletRequest request) {
		HttpSession session = request.getSession();
		Object o = session.getAttribute(AuthProviderDAO.KEY_AUTH);
		if (o == null) { return null; }
		if (!(o instanceof WitsUser)) {
			session.removeAttribute(AuthProviderDAO.KEY_AUTH);
			return null;
		} else {
			return (WitsUser) o;
		}
	}

	private void audit(String action, WitsUser user, String... tokens) {
		if (auditLogger.isInfoEnabled()) {
			StringBuilder message = new StringBuilder(toAllCaps(action));
			message.append(DELIM).append(user.getLoginId()).
				append(DELIM).append(user.getLoginIp()).
				append(DELIM).append(user.getClientBrowser());
			if (tokens != null) { message.append(DELIM).append(messages.getMessage(action, tokens, null)); }
			auditLogger.info(message.toString());
		}
	}

	private void audit(String action, HttpServletRequest request, AuthDTO auth, String... tokens) {
		if (auditLogger.isInfoEnabled()) {
			StringBuilder message = buildMessage(action, request, auth);
			message.append(messages.getMessage(action, tokens, null));
			auditLogger.info(message.toString());
		}
	}

	private void auditFailCount(HttpServletRequest request, AuthDTO auth) {
		StringBuilder message = buildMessage("auth.failed", request, auth);

		HttpSession session = request.getSession();
		Object failCount = session.getAttribute(AuthProviderDAO.KEY_FAIL_COUNT);
		if (failCount != null) {
			message.append(DELIM).
				append(messages.getMessage("auth.failed", new Object[]{failCount}, request.getLocale()));
		}

		auditLogger.warn(message.toString());
	}

	private static void incrementFailCount(HttpServletRequest request) {
		HttpSession session = request.getSession();
		Object failCount = session.getAttribute(AuthProviderDAO.KEY_FAIL_COUNT);
		session.setAttribute(AuthProviderDAO.KEY_FAIL_COUNT, failCount == null ? 1 : ((Integer) failCount) + 1);
	}

	private static StringBuilder buildMessage(String action, HttpServletRequest request, AuthDTO auth) {
		StringBuilder message = new StringBuilder(toAllCaps(action));
		message.append(DELIM).append(auth.getUsername()).
			append(DELIM).append(request.getRemoteAddr()).
			append(DELIM).append(request.getHeader("User-Agent")).
			append(DELIM);
		return message;
	}

	private static String toAllCaps(String action) {
		return action.replaceAll("^.*\\.", "").replaceAll("([A-Z])", "_$1").toUpperCase();
	}

}